# Useful documentation for SQLAlchemy ORM:
#     https://docs.sqlalchemy.org/en/13/orm/tutorial.html
#     https://docs.sqlalchemy.org/en/13/orm/basic_relationships.html

import flask
import json
import pagure.lib.model
import random
import string

from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, \
                       LargeBinary, UnicodeText, func
from sqlalchemy import PrimaryKeyConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

from . import activitypub
from . import database
from . import settings
from . import tasks
from Crypto.PublicKey import RSA
from pagure.config import config as pagure_config

# The app URL defined in the Pagure configuration, eg. "https://example.org/"
# We need this for generating Actor IDs.
APP_URL = pagure_config['APP_URL'].rstrip('/')
assert APP_URL and len(APP_URL) > 0, 'APP_URL missing from Pagure configuration.'

# We use SQLAlchemy "declarative" mappings: https://docs.sqlalchemy.org/en/13/orm/tutorial.html#declare-a-mapping
BASE = declarative_base()

def new_random_id(length=32):
    """
    Generate a random string to use as an Activity ID when a new one is
    created.
    """
    
    symbols = string.ascii_lowercase + string.digits
    return ''.join([ random.choice(symbols) for i in range(length) ])

class Actor:
    @property
    def inbox_id(self):
        return self.actor_id + '/inbox'
    
    @property
    def outbox_id(self):
        return self.actor_id + '/outbox'
    
    @property
    def followers_id(self):
        return self.actor_id + '/followers'
    
    @property
    def following_id(self):
        return self.actor_id + '/following'
    
    @property
    def publickey_id(self):
        return self.actor_id + '/key.pub'
    
    def _deliver(self, activity):
        """
        Send an activity.
        https://www.w3.org/TR/activitypub/#delivery
        
        :param activity: Python dictionary of the Activity to send.
        """
        
        activity_document = json.dumps(activity)
        
        db = database.start_database_session()
        
        # Save a copy of the Activity in the database before sending
        db.add(Activity(id       = activity['id'],
                        actor_id = self.actor_id,
                        document = activity_document))
        
        # Save the Activity to the Actor OUTBOX
        # outbox = Outbox(actor_id    = self.actor_id,
        #                activity_id = activity.id)
        #db.add(outbox)
        
        # Close db session
        db.commit()
        db.remove()
        
        # Now we are ready to POST to the remote actors
        
        # First off, we extract the list of recipients and remove the bto:
        # and bcc: recipients from the Activity before it's sent.
        recipients = []
        
        if 'to' in activity:
            if isinstance(activity['to'], str):
                activity['to'] = [ activity['to'] ]
            
            recipients.extend(activity['to'])
        
        if 'cc' in activity:
            if isinstance(activity['cc'], str):
                activity['cc'] = [ activity['cc'] ]
            
            recipients.extend(activity['cc'])
        
        if 'bto' in activity:
            if isinstance(activity['bto'], str):
                activity['bto'] = [ activity['bto'] ]
            
            recipients.extend(activity['bto'])
        
            # Remove according to spec.
            # https://www.w3.org/TR/activitypub/#client-to-server-interactions
            del activity['bto']
        
        if 'bcc' in activity:
            if isinstance(activity['bcc'], str):
                activity['bcc'] = [ activity['bcc'] ]
            
            recipients.extend(activity['bcc'])
        
            # Remove according to spec.
            # https://www.w3.org/TR/activitypub/#client-to-server-interactions
            del activity['bcc']
        
        # Stop here if there are no recipients.
        # https://www.w3.org/TR/activitypub/#h-note-8
        if len(recipients) == 0:
            return
        
        # Create a new Celery task individual delivery to each recipient
        for recipient in recipients:
            tasks.post_activity.delay(
                activity_document  = activity_document,
                recipient_id       = recipient,
                indirections       = settings.DELIVERY_INDIRECTIONS)
    
    def accept(self, object_id):
        """
        Accept an ActivityPub object.
        """
        
        # Create the RDF document of the "Accept" Activity
        activity = {
            'id':     self.actor_id + '/outbox/' + new_random_id(),
            'type':   'Accept',
            'actor':  self.actor_id,
            'object': object_id
        }
        
        # Send the "Accept" Activity to the remote actors
        self._deliver(activity)

class Person(pagure.lib.model.User, Actor):
    @property
    def actor_id(self):
        return APP_URL + '/' + self.url_path
    
    def to_rdf(self):
        return {
            '@context':          activitypub.jsonld_context,
            'type':              'Person',
            'name':              self.fullname,
            'preferredUsername': self.username,
            'id':                self.actor_id,
            'inbox':             self.inbox_id,
            'outbox':            self.outbox_id,
            'followers':         self.followers_id,
            'following':         self.following_id,
            'publicKey':         self.publickey_id,
        }

class Repository(pagure.lib.model.Project, Actor):
    @property
    def actor_id(self):
        return APP_URL + '/' + self.url_path + '.git'
    
    def to_rdf(self):
        return {
            '@context':  activitypub.jsonld_context,
            'type':      'Repository',
            'name':      self.name,
            'id':        self.actor_id,
            'inbox':     self.inbox_id,
            'outbox':    self.outbox_id,
            'followers': self.followers_id,
            'following': self.following_id,
            'publicKey': self.publickey_id,
            'team':      None,
        }

class GpgKey(BASE):
    """
    This class represents a Person GPG key that is used to sign HTTP POST
    requests.
    """
    
    __tablename__ = 'forgefed_gpg_key'
    __table_args__ = (
        PrimaryKeyConstraint('actor_id', 'private'),
    )
    
    # The actor who owns the key
    actor_id = Column(UnicodeText, nullable=False)
    
    # The private part of the key
    private = Column(LargeBinary, nullable=False)
    
    # The public part of the key
    public = Column(LargeBinary, nullable=False)
    
    # When was this key created
    created = Column(DateTime, nullable=False, default=func.now())
    
    @staticmethod
    def new_key():
        """
        Returns a new private/public key pair.
        """
        
        key = RSA.generate(settings.HTTP_SIGNATURE_KEY_BITS)
        
        return {
            'private': key.export_key('PEM'),
            'public':  key.publickey().export_key('PEM')
        }
    
    @staticmethod
    def test_or_set(actor_id):
        """
        Test if an Actor already has a GPG key, otherwise automatically generate
        a new one.
        
        :param actor_id: ID (URL) of the ActivityPub Actor.
        """
        
        db = database.start_database_session()
        
        key = db.query(GpgKey) \
                .filter(GpgKey.actor_id == actor_id) \
                .one_or_none()
        
        if not key:
            key = GpgKey.new_key()
            
            db.add(GpgKey(actor_id = actor_id,
                          private  = key['private'],
                          public   = key['public']))
            
            db.commit()
        
        db.remove()

class Follower(BASE):
    """
    This class represents a "Follow" relationship.
    """
    
    __tablename__ = 'forgefed_follower'
    __table_args__ = (
        PrimaryKeyConstraint('subject_id', 'object_id'),
    )
    
    # Actor that is following
    subject_id = Column(UnicodeText, nullable=False)
    
    # Actor that is followed
    object_id = Column(UnicodeText, nullable=False)
    
    # When was this relation established
    established = Column(DateTime, nullable=False, default=func.now())

class Activity(BASE):
    """
    This class represents an ActivityPub Activity.
    """
    
    __tablename__ = 'forgefed_activity'
    
    # The ID of the Activity (a URL)
    id = Column(UnicodeText, primary_key=True, default=new_random_id)
    
    # The ID (URL) of the Actor who sent the Activity.
    actor_id = Column(UnicodeText, nullable=False)
    
    # The JSON-LD document of the activity
    document = Column(UnicodeText, nullable=False)
    
    # When an Activity was received
    received = Column(DateTime, nullable=False, default=func.now())
    
    deliveries = relationship('Delivery', back_populates='activity')
    #inboxes  = relationship('Inbox',  back_populates='activity')
    #outboxes = relationship('Outbox', back_populates='activity')

class Delivery(BASE):
    """
    This class/table records which Activity was delivered to which Actor.
    It records both incoming and outgoing activities.
    """
    
    __tablename__ = 'forgefed_delivery'
    __table_args__ = (
        PrimaryKeyConstraint('activity_id', 'recipient_id'),
    )
    
    # The ID of the Activity
    activity_id = Column(
        UnicodeText,
        ForeignKey('forgefed_activity.id'),
        nullable=False)
    
    # The ID (URL) of the Actor who received the Activity.
    recipient_id = Column(UnicodeText, nullable=False)
    
    # The ID of the INBOX used to send the Activity to the Actor. An Actor can
    # have both an "inbox" and "sharedInbox".
    recipient_inbox = Column(UnicodeText, nullable=False)
    
    # DateTime of when the Activity was delivered
    delivered = Column(DateTime, nullable=False, default=func.now())
    
    activity = relationship('Activity', back_populates='deliveries')

"""
class Inbox(BASE):
    # This class represents an Actor INBOX messages.
    
    __tablename__ = 'forgefed_inbox'
    __table_args__ = (
        PrimaryKeyConstraint('actor_id', 'activity_id'),
    )
    
    # The ID of the Actor (a URL)
    actor_id = Column(UnicodeText, nullable=False)
    
    # The ID of the Activity
    activity_id = Column(
        UnicodeText,
        ForeignKey('forgefed_activity.id'),
        nullable=False)
    
    # When an Activity was added to the Actor INBOX
    received = Column(DateTime, nullable=False, default=func.now())
    
    activity = relationship('Activity', back_populates='inboxes')

class Outbox(BASE):
    # This class represents an Actor OUTBOX messages.
    
    __tablename__ = 'forgefed_outbox'
    __table_args__ = (
        PrimaryKeyConstraint('actor_id', 'activity_id'),
    )
    
    # The ID of the Actor (a URL)
    actor_id = Column(UnicodeText, nullable=False)
    
    # The ID of the Activity
    activity_id = Column(
        UnicodeText,
        ForeignKey('forgefed_activity.id'),
        nullable=False)
    
    # When an Activity was added to the Actor OUTBOX
    received = Column(DateTime, nullable=False, default=func.now())
    
    activity = relationship('Activity', back_populates='outboxes')
"""


# Automatically create the database tables on startup. The tables are first
# checked for existence before any CREATE TABLE command is issued.
# https://docs.sqlalchemy.org/en/13/orm/tutorial.html#create-a-schema
BASE.metadata.create_all(database.engine)








